﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Data.Linq;
using System.Reflection;

namespace Close2Poco
{
    // just a marker interface
    public interface IClose2PocoEntity
    {}


    /// <summary>
    /// Supports bi-directional relationships in near-POCO classes
    /// </summary>
    public static class RelationshipHelper
    {
        #region Setup

        /// <summary>
        /// Initialises all EntitySets with instances of the approprate objects.  In particular, the EntitySets have delegates set up 
        /// to work their magic when things are added to or removed from the set. 
        /// </summary>
        /// <remarks> 
        /// The normal generated LINQ classes do this in their constructors.  We have no setup routine for entity refs because they ae structs so they
        /// impilcity have a default value, and that default value is acceptable.</remarks>
        public static void Create<T>(this IClose2PocoEntity entity, ref EntitySet<T> entitySet) where T:class
        {
            PropertyInfo property = IdentifyProperty(entity, ref entitySet);

            // Get the relationship, for which the entity set stores the "list" end
            Relationship relationship = RelationshipFinder.GetRelationship(property);  

            // Create add and remove delegates that know about the relationship
            // The lambdas we create here capture the entity and relationship local variables, and recived the child object as a parameter
            Action<T> onAdd    = childObject => AddChild(entity, childObject, relationship);
            Action<T> onRemove = childObject => RemoveChild(childObject, relationship);

            // Create an entity set of the right type, with the delegates
            entitySet = new EntitySet<T>(onAdd, onRemove);
        }


        #endregion

        #region Change to Entity Set

        /// <summary>
        /// Assigns new content into entity set.
        /// </summary>
        /// <remarks>We have this for 2 reasons (a) consistence with other property setters (b) current version of ActiveSharp needs property
        /// to use field by ref.  In theory, with a change to ActiveSharp, we could do away with reason (b) and possibly drop this routine,
        /// although is it inconsisent then?</remarks>
        public static void SetValue<T>(this IClose2PocoEntity entity, ref EntitySet<T> entitySet, IList<T> value) where T: class
        {
            entitySet.Assign(value);
        }

        /// <summary>
        /// This is automatically called by the entity set, when an object is added to it
        /// </summary>
        static void AddChild(object entity, object childObject, Relationship r)
        {
            //todo: do we need/want to send list changed notification evets here.... Maybe call something wher end-programmer can do thier own list changed stuff...
            r.RefEnd.SetValue(childObject, entity, null);  // set "entity" as the new parent of the child
            // TODO: profile, and remove the reflection if too slow
        }

        static void RemoveChild(object childObject, Relationship r)
        {
            //todo: do we need/want to send list changed notification evets here.... Maybe call something wher end-programmer can do thier own list changed stuff...
            r.RefEnd.SetValue(childObject, null, null);  // set "null" as the new parent of the child
            // TODO: profile, and remove the reflection if too slow
        }

        #endregion


        #region Change to entity ref

        /// <summary>
        /// Sets the content of an entity ref, while synhronising the other end of the relationship.
        /// </summary>
        /// <remarks>We are setting the end that points to one object; the other end has the list</remarks>
        public static void SetValue<T>(this IClose2PocoEntity entity, ref EntityRef<T> entityRef, T value) where T: class
        {
            // Figure out which property has been changed.  Best to do this first, because we cannot guarantee state of object when this executes (e.g. entityRef might be null, since this executes on poorly/partiually constructed "dummy" objects
            PropertyInfo property = IdentifyProperty(entity, ref entityRef);

            // check previous value
            // while it seems counter-inituitive to actually force a lazy load at this point,
            // it is necessary to do so, for the reasons noted here: http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=2663423&SiteID=1&mode=1 
            // This check of HasLoadedOrAssignedValue is also based on the standard LINQ generated code
            T previousValue = entityRef.Entity;
            if (previousValue == value && entityRef.HasLoadedOrAssignedValue)  // we really do have an old value, and it is the same as the new one
                return;                                                        

            // get the relationship that this property belongs to (usually a bidirectional relationship, but not necessarily.  May have just a "ref end" - in which case, we still need it's information about the keys at the ref end
            Relationship relationship = RelationshipFinder.GetRelationship(property);

            if(previousValue != null)
            {
                IList otherEndOfRelationship = GetList( previousValue, relationship.ListEnd);
                entityRef.Entity = null;                 
                if (otherEndOfRelationship != null)
                    otherEndOfRelationship.Remove(entity);
            }

            entityRef.Entity = value;

            if(value == null)
            {
                ClearLocalKey(entity, relationship);
            }
            else
            {
                IList otherEndOfRelationship = GetList(value, relationship.ListEnd);
                if (otherEndOfRelationship != null)
                {
                    // we want to do this: otherEndOfRelationship.Add(this);
                    // but unfortunately, EntitySet's IList.Add is different from its "native"/own .Add, and we need to call the latter. Only the lattre works in the way we need. Specifically, it supresses duplicates, and it correctly hooks into the logic for handling potential mutual recursion between this end of the link and the other
                    // BTW the difference is a bug.  See http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=2656903&SiteID=1
                    MethodInfo addMethod = otherEndOfRelationship.GetType().GetMethod("Add");  // get the EntitySet's real Add method
                    addMethod.Invoke(otherEndOfRelationship, new object[] {entity});
                    //TODO: profile and remove reflection if too slow.. Cosnider adding a sutiable method to EntitySetUtil
                }
                SetLocalKey(entity, relationship, value);
            }
        }

        static IList GetList(object containingObject, PropertyInfo listProperty)
        {
            // return no list at all, if there is not list property (i.e. if the relationship was one-way)
            if (listProperty == null)
                return null;

            object result = listProperty.GetValue(containingObject, null);
            return (IList)result;                                  
        }

        static void ClearLocalKey(object entity, Relationship r)
        {
            for (int i = 0; i < r.KeyAtRefEnd.Count; i++)
                r.KeyAtRefEnd[i].SetValue(entity, null, null);    // assumes that all elements of local key are nullable.  I think we can probably rely on this...
                                                                // TODO: check that this works with nullable structs
        }

        static void SetLocalKey(object entity, Relationship r, object referencedObject)
        {
            for (int i = 0; i < r.KeyAtListEnd.Count; i++)
                SetKeyProperty(entity, r.KeyAtListEnd[i], r.KeyAtRefEnd[i], referencedObject);
        }

        /// <summary>
        /// Copy from primary key of referenced object to local key element in this object
        /// </summary>
        static void SetKeyProperty(object entity, PropertyInfo sourceProp, PropertyInfo destinationProp, object referencedObject)
        {
            //TODO: profile this.  If the reflection cost matters to us, then look into tuning options 
            object value = sourceProp.GetValue(referencedObject, null);
            destinationProp.SetValue(entity, value, null);
        }

        #endregion

        #region Change to other things

        public static void SetValue<T>(this IClose2PocoEntity entity, ref T field, T value)
        {
            // todo prop change notification here (with opt-in by user/caller)
        }


        #endregion

        /// <summary>
        /// Identify the property that corresponds to a given field.
        /// </summary>
        /// <remarks>
        /// We identify the property by finding the offset off the property's field
        /// from some known field.  Any field (in this object) will do.
        /// We have a dummy one in the initial implementation, but could us one related 
        /// to INotifyPropertyChanging/ed if we ever support those interfaces
        /// </remarks>
        static PropertyInfo IdentifyProperty<T>(object entity, ref T field)
        {
            return ActiveSharp.PropertyMapping.PropertyMap.GetProperty(entity, ref field);
        }

    }

    //todo: by the time this class runs, we have the LINQ metamodel.  Consuider using it isntead of our own "Relationship" class at this pioint?,
    // but bear in mind cachabvility and trheadsafely (or otherwise) and cost of construction, of the mapping spource and model classes
}
